Starting Assumptions

We're going to model the cost of one specific home, and compare it to an equivalent rent. The house is here. I live in this area, and represent that the asking price, Zillow rent estimate, and financing terms are all reasonable.

So here's some rent estimates:


In [1]:
rent_per_month = 1695
rent_increase_per_month = 50.0 / 12

Zillow has also helpfully calculated the 30-year fixed mortgage terms:


In [2]:
price = 216000
down = price * 0.20
apr = 0.03411
term = 12 * 30
yearly_taxes = 3317

MATH TIME

First, the fundamental law of finance. Net present value of any sum of money:


In [3]:
def npv(future_value, discount_rate, t):
    return future_value / (1 + discount_rate) ** t

Calculating the net present value of all rental costs is fairly simple. Assume we're paying one month's rent as a deposit, and discount each month's rent:


In [4]:
def rent(r, months):
    def cost_of_month(t):
        return rent_per_month + (rent_increase_per_month * t)
    deposit = rent_per_month
    deposit_back = npv(deposit, r, months)
    return -deposit + sum(-npv(cost_of_month(t), r, t) for t in xrange(0, months)) + deposit_back

So, after 10 years, we will pay:


In [5]:
rent(0, 12 * 10)


Out[5]:
-233150.0

Sounds about right.

Calculating the cost of buying is obviously more involved. Let's start with some basic mortgage definitions:


In [6]:
class Mortgage:
    def __init__(self, down, total, payments, rate):
        self.down, self.total, self.payments, self.rate = down, total, payments, rate
        
    def monthly_payment(self):
        r = self.rate
        P = self.total - self.down
        N = self.payments
        # From https://en.wikipedia.org/wiki/Mortgage_calculator#Monthly_payment_formula
        return (r * P)  / (1 - (1 + r) ** -N)
        
    def remaining(self, t):
        balance = self.total - self.down
        for _ in xrange(0, t):
            interest = balance * self.rate
            balance -= (self.monthly_payment() - interest)
        return max(0, balance)

And plug in the terms Zillow gave us:


In [7]:
mortgage = Mortgage(down, price, term, apr / 12)

Now let's do some basic sanity checking:


In [8]:
mortgage.monthly_payment()


Out[8]:
767.3899126385163

In [9]:
mortgage.remaining(12 * 30)


Out[9]:
7.64271135267336e-09

In [10]:
mortgage.remaining(0)


Out[10]:
172800.0

All looking good.

Now we can model the cost of buying. We need to pay:

  • money down for the mortgage
  • closing costs on purchase, including agent fees, inspection, etc
  • closing costs on sale
  • monthly maintenance, monthly mortgage
  • taxes (there are actually some important tax deductions you can claim against mortgage interest and PMI, but we're going to ignore those for simplicity)

In [11]:
def buy_in_general(r, months, mortgage, buy_closing_costs, sale_closing_costs, maintenance, taxes, appreciation):
    cost_to_close = mortgage.down + (mortgage.total * buy_closing_costs)
    
    mortgage_cost = sum(npv(mortgage.monthly_payment(), r, t) for t in xrange(0, min(months, mortgage.payments)))
    maintenance = sum(npv(maintenance * mortgage.total / 12, r, t) for t in xrange(0, months))
    
    taxes = sum(npv(taxes, r, year * 12) for year in xrange(months / 12))
    
    owed_bank = mortgage.remaining(months)
    from_buyer = mortgage.total * appreciation
    cash_at_sale = from_buyer - owed_bank - (from_buyer * sale_closing_costs)
    
    return -cost_to_close - mortgage_cost - maintenance - taxes + npv(cash_at_sale, r, months)

Let's assume:

  • Closing costs are 1% on buying, and 6.5% on sale. Costs are dramatically higher on sale because the sale transaction is typically structured so that agent commissions are "paid" by the seller out of the price of the home.
  • Maintenance is 1% per year.
  • The home does not appreciate in value whatsoever during the term of the investment (which is arguably pessimistic).

In [12]:
def buy(r, months,
        mortgage=mortgage,
        buy_closing_costs=0.01,
        sell_closing_costs=0.065,
        maintenance=0.01,
        taxes=yearly_taxes,
        appreciation=1.0): 
    return buy_in_general(r, months,
                          mortgage, buy_closing_costs, sell_closing_costs,
                          maintenance, yearly_taxes, appreciation)

Let's see how much this house will cost us, undiscounted, over several years:


In [13]:
[buy(0, y * 12) for y in xrange(1, 11)]


Out[13]:
[-27518.89626350197,
 -38721.139054030995,
 -49802.686520948744,
 -60759.356770151964,
 -71586.82301179678,
 -82280.60853990016,
 -92836.0815379926,
 -103248.44970479555,
 -113512.75469368682,
 -123623.86635950336]

Let's compare that to renting:


In [14]:
[rent(0, y * 12) for y in xrange(1, 11)]


Out[14]:
[-20615.0,
 -41830.0,
 -63645.0,
 -86060.0,
 -109075.0,
 -132690.0,
 -156905.0,
 -181720.0,
 -207135.0,
 -233150.0]

So, renting becomes more expensive quickly, if we assume our money has no time value. That's not a good assumption, so let's delve a little deeper.

IRR

IRR is a common concept when evaluating investments. The IRR essentially states the smallest time value your money needs to have for you to lose money on your investment. In other words, you want to know the rate that causes your discounted cash flow to become negative.


In [15]:
def irr(dcf):
    if dcf(0) < 0: return
    for ix in xrange(1000):
        r_t = float(ix) / 1000
        if dcf(r_t) < 0:
            return r_t

Note that there are cases where there is no IRR. Your investment loses money, even assuming your upfront dollars are valued equally to later hypothetical dollars. It cannot grow your wealth.

There will be cases where this happens with our house investment.

We can model this "investment" by subtracting the rent we're not paying from the other costs that we now must pay. Now, (drumroll please), let's look at the IRR for our hypothetical house, depending on how long we stay in it:


In [16]:
for year in xrange(1, 16):
    def dcf(r_i): return buy(r_i / 12, 12 * year) - rent(r_i / 12, 12 * year)
    print year, irr(dcf)


1 None
2 0.039
3 0.109
4 0.142
5 0.161
6 0.173
7 0.18
8 0.185
9 0.189
10 0.192
11 0.193
12 0.195
13 0.196
14 0.196
15 0.197

Not bad. We're well above the paltry 1.3% current risk free rate in year three. By year four, our return rises above the 10% average annual returns for the S&P 500.

There's obviously a couple caveats. First, all of this analysis is predicated on the house being a principle residence. There are many benefits, especially regarding capital gains, that flow from the government in this case. Investment properties are a whole 'nother ball of wax.

Finally, let's keep in mind that this investment scenario has one big caveat: we disappear into thin air after we sell the house, and never spend money on housing again. We'll return to that assumption later.

Bring on the Bears

Of course, market conditions can change drastically and quickly. What does our investment look like in a super-pessimistic case, where our house loses half its value?


In [17]:
def buy_ohno(r, months):
    return buy(r, months, appreciation=0.5)

In [18]:
for year in xrange(1, 21):
    def dcf(r_i): return buy_ohno(r_i / 12, 12 * year) - rent(r_i / 12, 12 * year)
    print year, irr(dcf)


1 None
2 None
3 None
4 None
5 None
6 None
7 None
8 None
9 None
10 0.059
11 0.109
12 0.135
13 0.152
14 0.164
15 0.172
16 0.178
17 0.182
18 0.186
19 0.188
20 0.19

Pretty grim, but still no doomsday. We'll need to stay in the house for a long time to beat the stock market if we get caught in a bad market cycle.

The Long View

We've established that the longer you stay in the house, the better off you are. Let's look at what happens after your mortgage is payed off, and if your house reliably appreciates to keep up with a 2% inflation rate over the years.


In [19]:
for year in xrange(1, 31, 5):
    def buy_and_appreciate(r, months): 
        return buy(r, months, appreciation=1.02 ** year)
    def dcf(r_i): return buy_and_appreciate(r_i / 12, 12 * year) - rent(r_i / 12, 12 * year)
    print year, irr(dcf)


1 None
6 0.216
11 0.214
16 0.207
21 0.203
26 0.2

What? Our rate of return gets worse the longer we wait to sell?

Because the house appreciates less quickly than our return rate, it makes sense to sell it ASAP. We're forgetting that we actually have to live somewhere if we don't own a house. Let's do a more realistic comparison: whenever we sell our house, we'll buy another similar house somewhere else. So we'll be taking out a series of mortgages with the following guidelines:

  • We want to retire someday, so we structure our payments to zero the mortgage after thirty years no matter what.
  • We plow all of our equity from before into the new mortgage, minus closing costs.
  • If we don't have enough equity to cover closing costs, we assume we can pony up the difference.

In [20]:
total_months = 50 * 12 
mortgage_months = 30 * 12
appreciation = 0.02

def mortgages(swap_months, appreciation):
    prior_mortgage = None
    swaps = [m for m in xrange(swap_months, total_months, swap_months)]
    
    for month in [0] + swaps:
        new_term = mortgage_months - month
        if new_term > 0:
            new_price = price * (1 + appreciation) ** month
            new_down = new_price * 0.20
            if not prior_mortgage:
                equity = down
            else:
                equity = new_price - prior_mortgage.remaining(swap_months) - 0.075 * new_price

            prior_mortgage = Mortgage(max(equity, new_down), new_price, new_term, apr / 12)
            yield month, prior_mortgage

Let's look at how our equity changes in the first years:


In [21]:
for year in (1, 2, 5, 10, 25, 50):
    print year, list(m.total - m.remaining(year * 12) for _, m in mortgages(year * 12, appreciation / 12))[:5]


1 [46566.78268816022, 47696.32722958573, 48867.46325427029, 50083.41586767594, 51347.8655691084]
2 [50050.21884929339, 52908.73235120109, 56058.59938981806, 59561.54823595419, 63501.08063338252]
5 [61241.57174651415, 89902.21983048532, 127260.19030345339, 177214.64565176948, 247025.85305243937]
10 [82632.92315711873, 174287.08247778635, 322126.8603694791]
25 [173724.0250192302, 355975.6050638522]
50 [216000.0]

Yikes, we're barely building any equity at all when we only switch every 1-2 years. We're probably ponying up a ton of closing costs too. Now let's look at what our final monthly payments look like:


In [22]:
for year in (1, 2, 5, 10, 25, 50):
    print year, list(m.monthly_payment() for _, m in mortgages(year * 12, appreciation / 12))[-5:]


1 [4331.454440777877, 4939.288618283636, 5752.405639125483, 6975.983313653811, 9430.730631788698]
2 [2081.1559470156953, 2380.8092512348812, 2783.2571197636807, 3391.0912972694505, 4614.668971797793]
5 [856.1607031760279, 969.9939340948902, 1125.3296002131922, 1363.2271158811006, 1847.8500404778238]
10 [767.3899126385163, 881.2231435573807, 1119.1206592252965]
25 [767.3899126385163, 1252.0128372353004]
50 [767.3899126385163]

Wow, those one and two year scenarios look pretty grisly. We have to really pay to catch our equity back up. Even the final payments for five-year switching don't look too great, but they're still manageable.

The math is basically reinforcing common sense. It makes very little finanacial sense to constantly house swap. Let's look at the rate of return for the sensible swapping intervals:


In [23]:
for year in (5, 10, 25, 50):
    swap_months = year * 12
    
    def buy_and_appreciate(r, term, mortgage): 
        return buy(r, term, mortgage=mortgage, appreciation=1.0)
    
    def dcf_swapping_houses(r):
        mortgage_list = list(mortgages(swap_months, appreciation / 12))
        buy_costs = [npv(buy_and_appreciate(r / 12, swap_months, m), r / 12, month)
                     for month, m in mortgage_list[:-1]]
        
        final_month, final_mortgage = mortgage_list[-1]
        mortgage_remaining = 30 * 12 - swap_months
        months_to_final = final_month
        months_remaining = total_months - months_to_final
        final_cost = npv(buy_and_appreciate(r / 12, months_remaining, final_mortgage), r / 12, months_to_final)
        
        return sum(buy_costs) + final_cost - rent(r / 12, total_months)

    print year, irr(dcf_swapping_houses)


5 0.128
10 0.162
25 0.193
50 0.197

Not bad. If we're willing to put up with some pretty high catch-up payments at the end of our 30-year term, we easily beat the stock market. Even if we switch houses every five years.

If we're planning on staying put for a very long time, a house looks like it could be a fantastic investment.

Another thing to keep in mind: with this time horizon, the final sale price of the house itself is practically inconsequential. Here's the present value of the cash we'll get on sale, discounted at 10% per year:


In [24]:
npv(price * (1 + appreciation) ** 50, 0.1 / 12, 50 * 12)


Out[24]:
3999.334945518201

This is a tiny fraction of the total NPV of our housing costs:


In [25]:
buy(0.1 / 12, 50 * 12)


Out[25]:
-188527.62346588002

The 50-year time frame is pretty extreme. Many things can change. That's one of the reasons we give money time preference in the first place.

TL;DR

So is housing a shitty investment? It certainly can be, especially if you change houses every year. But if you're in a specific location for the long haul, investment in housing for your principle residence will probably beat market returns.

Another final note: the current lending environment is very favorable towards buying a house. Interest rates are at all time lows. All the analysis here will certainly need to change in the future.